package space.misiro.ledgers.token.exchange

import jakarta.ws.rs.Consumes
import jakarta.ws.rs.OPTIONS
import jakarta.ws.rs.POST
import jakarta.ws.rs.Produces
import jakarta.ws.rs.core.Context
import jakarta.ws.rs.core.HttpHeaders
import jakarta.ws.rs.core.MediaType
import jakarta.ws.rs.core.MediaType.APPLICATION_JSON
import jakarta.ws.rs.core.Response
import org.jboss.logging.Logger
import org.keycloak.TokenVerifier
import org.keycloak.common.VerificationException
import org.keycloak.crypto.SignatureProvider
import org.keycloak.events.EventBuilder
import org.keycloak.http.HttpRequest
import org.keycloak.models.*
import org.keycloak.protocol.oidc.TokenManager
import org.keycloak.representations.AccessToken
import org.keycloak.representations.AccessTokenResponse
import org.keycloak.services.Urls
import org.keycloak.services.managers.AppAuthManager
import org.keycloak.services.resource.RealmResourceProvider
import org.keycloak.services.resources.Cors
import org.keycloak.services.util.DefaultClientSessionContext.fromClientSessionScopeParameter

@SuppressWarnings("PMD")
class ConfigurableTokenResourceProvider(
    val session: KeycloakSession,
    val tokenManger: TokenManager = TokenManager()
) : RealmResourceProvider {
    override fun getResource() = this

    override fun close() {
    }

    @OPTIONS
    fun preflight() = Cors.add(session.context.httpRequest, Response.ok())
        .auth()
        .preflight()
        .allowedMethods("POST", "OPTIONS")
        .build()

    @POST
    @Consumes(APPLICATION_JSON)
    @Produces(APPLICATION_JSON)
    fun createToken(tokenConfiguration: TokenConfiguration) = try {
        val context = session.context
        val request = context.httpRequest
        val accessToken = validateTokenAndUpdateSession(request)
        val userSession = findSession()
        val response = createAccessToken(userSession, accessToken, tokenConfiguration)
        buildCorsResponse(request, response)
    } catch (e: ConfigurableTokenException) {
        LOG.error("An error occurred when fetching an access token", e)
        Response.status(Response.Status.BAD_REQUEST).build()
    }

    private fun buildCorsResponse(@Context request: HttpRequest, response: AccessTokenResponse) =
        Cors.add(request).auth().allowedMethods("POST").auth()
            .exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS, Cors.ACCESS_CONTROL_ALLOW_ORIGIN).allowAllOrigins()
            .builder(Response.ok(response).type(MediaType.APPLICATION_JSON_TYPE)).build()

    private fun findSession(): UserSessionModel {
        val authenticated = AppAuthManager.BearerTokenAuthenticator(session).authenticate()
            ?: throw ConfigurableTokenException("not_authenticated").also { LOG.warn("Keycloak-ConfigurableToken: user not authenticated") }

        authenticated.token.realmAccess ?: throw ConfigurableTokenException("wrong_realm").also {
            LOG.warn(
                "Keycloak-ConfigurableToken: no realm associated with authorization"
            )
        }

        authenticated.user?.takeIf { it.isEnabled }
            ?: throw ConfigurableTokenException("invalid_user").also { LOG.warn("Keycloak-ConfigurableToken: user does not exist or is not enabled") }

        return authenticated.session
            ?: throw ConfigurableTokenException("missing_user_session").also { LOG.warn("Keycloak-ConfigurableToken: user does not have any active session") }
    }

    private fun validateTokenAndUpdateSession(request: HttpRequest) = try {
        val realm = session.context.realm
        val tokenString = readAccessTokenFrom(request)
        val verifier = TokenVerifier.create(tokenString, AccessToken::class.java).withChecks(
            TokenVerifier.IS_ACTIVE,
            TokenVerifier.RealmUrlCheck(Urls.realmIssuer(session.context.uri.baseUri, realm.name))
        )
        val verifierContext = session.getProvider(SignatureProvider::class.java, verifier.header.algorithm.name)
            .verifier(verifier.header.keyId)
        verifier.verifierContext(verifierContext)
        verifier.verify().token.takeIf { tokenManger.checkTokenValidForIntrospection(session, realm, it, false) }
            ?: throw VerificationException("introspection_failed")
    } catch (e: ConfigurableTokenException) {
        throw e
    } catch (e: VerificationException) {
        LOG.warn("Keycloak-ConfigurableToken: introspection of failed", e)
        throw ConfigurableTokenException("access_token_introspection_failed: ${e.message}")
    }

    private fun readAccessTokenFrom(request: HttpRequest) =
        (request.httpHeaders.getHeaderString(HttpHeaders.AUTHORIZATION).takeIf { it.startsWith("Bearer ") }
            ?: throw ConfigurableTokenException("bearer_token_missing_in_authorization_header")
                .also { LOG.warn("Keycloak-ConfigurableToken: no authorization header with bearer token") })
            .substring(7).takeIf { it.isNotEmpty() }
            ?: throw ConfigurableTokenException("missing_access_token").also { LOG.warn("Keycloak-configurableToken: empty access token") }

    private fun createAccessToken(
        userSession: UserSessionModel,
        accessToken: AccessToken,
        tokenConfiguration: TokenConfiguration
    ): AccessTokenResponse {
        val realm = session.context.realm
        val client = realm.getClientByClientId(accessToken.issuedFor)
        LOG.info("Configurable token requested for username = ${userSession.user.username}, client = ${client.clientId}, realm = ${realm.name}")
        val clientSession = userSession.getAuthenticatedClientSessionByClient(client.id)
        val clientSessionContext = fromClientSessionScopeParameter(clientSession, session)

        val newToken = tokenManger.createClientAccessToken(
            session,
            realm,
            client,
            userSession.user,
            userSession,
            clientSessionContext
        )
        updateTokenExpiration(newToken, tokenConfiguration)
        updateScope(newToken, tokenConfiguration)
        return buildResponse(realm, userSession, client, clientSession, newToken)
    }

    private fun buildResponse(
        realm: RealmModel,
        userSession: UserSessionModel,
        client: ClientModel,
        clientSession: AuthenticatedClientSessionModel,
        token: AccessToken
    ): AccessTokenResponse {
        val eventBuilder = EventBuilder(realm, session, session.context.connection)
        val clientSessionContext = fromClientSessionScopeParameter(clientSession, session)
        return tokenManger.responseBuilder(realm, client, eventBuilder, session, userSession, clientSessionContext)
            .accessToken(token).build()
    }

    private fun updateTokenExpiration(token: AccessToken, tokenConfiguration: TokenConfiguration) =
        token.expiration(tokenConfiguration.computeTokenExpiration(token.exp.toInt(), true))

    private fun updateScope(token: AccessToken, tokenConfiguration: TokenConfiguration) {
        val offlineAccess = "offline_access".takeIf { token.scope.contains("offline_access") } ?: ""
        val updatedScope = "${token.scope} ${tokenConfiguration.scope}${offlineAccess}"
        token.scope = updatedScope.trim()
    }

    companion object {
        const val ID = "configurable-token"

        val LOG = Logger.getLogger(ConfigurableTokenResourceProvider::class.java)

        class ConfigurableTokenException(message: String) : Exception(message)
    }
}